deploy.sh 는 로컬에서 빌드해 서버에 새 릴리스로 올린다. 서버에 이미 ssh 가 되고 거기서 sudo -n 이 되는 기계에서 돌린다.
AHAWIKI_DEPLOY_HOST=<ssh host> AHAWIKI_HEALTH_HOST=<a site's domain> bash deploy.sh
어디에 배포하는지는 전부 환경변수에서 온다. 이 저장소는 공개라서 자기 기계 이름을 하나도 적지 않는다.
그것이 경계의 전부이고, 한 문장으로 적을 가치가 있다: 배포가 어떻게 도는지는 이 저장소가 갖고, 그것이 어느 기계에서 일어나는지는 운영을 담는 쪽이 갖는다. 단계·순서·헬스체크·태그 형식·롤백은 전부 여기, 그 모양을 결정하는 코드 옆에 있다. 호스트명·계정·앞단 리버스 프록시는 저쪽에 있고, 이것을 부르는 데는 값들과 한 줄이면 된다:
AHAWIKI_DEPLOY_HOST=… AHAWIKI_HEALTH_HOST=… AHAWIKI_VERIFY_URLS="… …" bash /path/to/AhaWiki/deploy.sh
절차의 사본을 저쪽에도 두면 같은 사실이 두 곳에 있게 되고, 반드시 갈라진다 — 이것이 대체한 사본은 아무도 읽지 않는 디렉터리에 쓸 만큼 낡아 있었다.
| 필수 — ssh 호스트 또는 alias |
| 필수 — 위키 중 하나가 답하는 도메인. |
|
|
|
|
|
|
| 끝나고 확인할 공개 URL 들, 공백 구분. 첫 URL 은 5단계에서도 쓴다 — 인스턴스 하나를 재시작한 뒤 프록시가 다시 답할 때까지 이것을 기다린다 |
|
|
|
|
릴리스는 releases/ 아래 나란히 놓이고 current 가 그중 하나를 가리키는 심볼릭 링크라서, 배포는 링크를 옮기는 것이고 롤백은 도로 옮기는 것이다:
H=<host>; S=<a site's domain> ssh $H "sudo -n -u ahawiki ln -sfn /opt/ahawiki/releases/<previous> /opt/ahawiki/current" for p in 10001 10000; do ssh $H "sudo -n systemctl restart ahawiki@$p" until [ "$(ssh $H "curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:$p/hc")" = 200 ]; do sleep 3; done until [ "$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 https://$S/)" = 200 ]; do sleep 3; done done
"하나 재시작, sleep 20, 다른 하나 재시작" 보다 길고, 두 until 줄이 그 이유다. 롤백은 이미 무언가 잘못됐을 때 도는데, 그때는 20초가 모자랐다는 것을 발견하기에 최악의 순간이다 — 인스턴스가 4초 만에 돌아왔는데 20초를 앉아 기다리는 것도 마찬가지다. 두 줄은 deploy.sh 가 기다리는 두 조건 그대로이고(이유는 아래), 둘째 줄은 군더더기가 아니다: loopback 에서 답하는 인스턴스가 곧 프록시가 트래픽을 보내 주는 인스턴스인 것은 아니다. 이 문서의 롤백 절차는 2026-08-16 까지 sleep 20 이라고 적혀 있었다 — 배포 경로가 초 세기를 그만둔 지 한참 뒤까지.
cache/ 와 logs/ 는 shared/ 에 살고 각 릴리스에 링크로 들어간다. 어느 릴리스보다도 오래 살기 때문이다. systemd 인스턴스 둘(ahawiki@<port>)이 리버스 프록시 뒤에 있다. 둘 다 요청을 받는다 — 2026-09-10 에 [[Uptime]] 을 여덟 번 렌더링하니 JVM 시작 시각 둘이 5:3 으로 번갈아 나왔다. /w/ 를 연달아 부르면 봇 차단에 걸리므로 /w/ 밖의 경로로 잰다:
for i in 1 2 3 4 5 6 7 8; do curl -s -X POST -H 'content-type: application/json' -d '{"comment":"[[Uptime]]"}' https://ahawiki.net/api/renderAhaMark/FrontPage; echo; sleep 1; done | sort | uniq -c
그래서 인스턴스마다 따로 있는 메모리 상태는 서로를 모른다 — 캐시는 Dev Cache 의 «인스턴스가 둘이다», 실시간 이벤트는 Dev WebSocket.
AHAWIKI_KEEP_RELEASES 를 넘는 옛 릴리스는 마지막에 지워진다. current 가 가리키는 것은 건너뛴다. 스크립트의 모든 원격 블록은 set -e 로 시작하고, 지우는 블록이 그것이 여전히 그런지 확인해야 하는 이유다. 없으면 releases/ 로의 cd 가 실패해도 아무것도 멈추지 않는다: 루프는 로그인 사용자의 홈 디렉터리를 목록하며 계속 돌고, 거기서는 "이것이 current 인가" 검사에 아무것도 걸리지 않아, 눈에 띄는 가장 오래된 항목들에 sudo rm -rf 를 돌린다. 실패가 아무도 종료코드를 읽지 않는 명령 안에 있어서, 배포는 어느 쪽이든 성공을 보고한다.
각각이 한 번씩 배포를 부순 값이다.
/hc 를 찌른다. 위키 페이지가 아니다. /hc 는 SELECT 1 로 DB 를, 그리고 디스크 여유를 확인하고 OK 를 답한다 — 재시작이 깨뜨릴 수 있는 것들이다. 사이트 매칭을 타지 않으니 Host 헤더도 필요 없다. 그리고 /w/ 밖에 있어야 한다. IpRateLimiter 는 /w/ 아래를 페이지 조회로, 나머지를 사람 신호로 세고, 짧은 창 안에서 페이지만 여러 번 요청한 주소를 차단한다(임계값과 그 이름은 Dev BotDetection). deploy.sh 의 폴링 간격(sleep 3)으로 /w/FrontPage 만 부르는 것이 정확히 그 정의다. 2026-09-04 에 이 루프가 다섯 번째 폴링에서 자기를 차단했다 — 남은 폴링은 403 과 tarpit 을 받았고, 인스턴스가 독자에게 정상 서비스하는 동안 배포는 never became healthy 로 죽었다. 차단은 IpDeny 에 행으로 남고 두 인스턴스가 그 테이블을 공유하므로, 고치지 않았다면 보관 기간 내내 모든 배포가 같은 자리에서 실패했다.
기동이 15초 안에 끝나면 네 번으로 통과한다. 그래서 몇 달간 멀쩡했고, 릴리스가 무거워지자 걸렸다. 폴링 창은 AHAWIKI_HEALTH_TRIES(기본 60 × 3초 = 180초)다. 2026-09-15 에는 두 인스턴스가 같이 뜨며 CPU 를 다투자 차가운 JVM+Play 기동이 첫 대에서 90초(옛 30회 고정)를 넘겨, 멀쩡한 기동을 배포가 실패로 보고하고 한 대를 옛 릴리스에 남긴 채 멈췄다 — 그때는 current 가 이미 새 릴리스를 가리키므로, 남은 한 대를 손으로 재시작해(새 릴리스를 집는다) 마쳤다. 더 느린 장비는 이 값을 올린다. 사이트가 실제로 그려지는지는 5단계의 프록시 기다림과 6단계가 프록시를 통해, 화이트리스트에 든 주소에서 확인한다 — /hc 폴링이 넓게 볼 이유가 없다. 7단계는 태그일 뿐 아무것도 확인하지 않는다.
tar over ssh 다. Git Bash 에서 MSYS2 rsync 를 부르면 두 MSYS 런타임을 건너느라 인자가 뭉개져 도착하고, 아무것도 복사하기 전에 죽는다.conf/evolutions 가 바뀐 배포는 백업을 먼저 본다. 스크립트는 이것을 하지 않는다 — 어느 데이터베이스인지가 운영 쪽 사실이라서다. 확인 명령과 멈춰야 할 값은 운영 저장소 ahawiki/README.md 에 있고, 읽기 전용 한 줄이다. 2026-09-02 에 evolution 둘이 이것 없이 나갔다. 안전망은 있었지만 그것을 안 것은 배포 뒤였고, 그날 두 번째 배포는 실제로 실패해 되돌려야 했다(Dev Database).sbt stage 는 conf/ 아래 모든 파일을 릴리스에 복사한다. gitignore 는 거기 관여하지 못한다 — git 이 무엇을 추적하는지를 다스릴 뿐, 빌드가 무엇을 패키징하는지는 아니다 — 그래서 거기 둔 로컬 설정은 배포마다 서버로 따라간다.
가정이 아니다. 개발 DB 비밀번호와 play.http.secret.key 가 릴리스 세 개에, world readable 로, 앱이 열지도 않는 파일에 앉아 있었다: 앱은 릴리스 밖을 가리키는 절대 경로 -Dconfig.file 로 시작된다. 지금 구조가 시작된 뒤로 배포마다 나가고 있었다.
build.sbt 는 이제 git 에게 conf/ 아래 무엇을 추적하는지 물어 그것만 패키징한다. ignore 된 파일은 실리지 않는다 — 모두가 이미 그런 줄 알았던 바로 그것이다. 무엇을 뺐는지도 말한다:
[info] conf/: not packaged (untracked): conf/something.local.conf
파일명 패턴을 먼저 시도했고, git 이 없는 빌드의 폴백으로만 남겼다. 패턴은 누군가 떠올린 모양들을 잡는데, 이 일의 발단이 된 파일은 그중 어느 모양도 아니었다: 호스트명을 따서 지은 이름이었다.
그 폴백은 2026-08-15 에 처음 실제로 돌려 봤다 — git 을 PATH 에서 빼고, 미끼 파일 둘을 심어서: 하나는 *.local.*, 하나는 호스트명을 딴 이름. 설계대로 동작했고 한계도 위에 적은 그대로다: 첫째는 빠졌고 둘째는 실렸다. 그래서 폴백은 경고하고, 이제 패키징하려는 conf/ 최상위 목록도 찍는다. "릴리스를 확인하라" 는 뺀 것의 목록으로는 답할 수 없기 때문이다 — 걱정할 파일은 정의상 패턴이 알아보지 못한 그 파일이다.
[warn] conf/: git unavailable, falling back to name patterns. Check the release for local configs. [warn] conf/: packaged on the name rule alone: conf/application.conf [warn] conf/: packaged on the name rule alone: conf/base.conf ...
여섯 줄쯤이고, 낯선 설정 하나는 거기 끼면 눈에 띈다. 하위 디렉터리는 그 목록에서 뺐다 — 기본 페이지와 evolution 전부는 하나를 숨기기에 충분히 길다. git 이 있으면 이것들은 하나도 나타나지 않는다. 보통의 빌드가 전부 그쪽이다.
배포 설정이 conf/ 에 살아야 할 이유는 아무것도 없다 — 서버는 절대 경로로 설정 위치를 받는다 — 그러니 가장 안전한 자리는 여전히 아예 다른 곳이다.
2026-08-12 까지 이 저장소에는 다른 배포 장치 둘이 실려 있었고 둘 다 더는 서버와 맞지 않았다: 옛 경로에 빌드를 덮어 복사하는 스크립트 하나, 서버에서 빌드해 pm2 로 띄우는 GitHub Actions 워크플로 하나. 운영은 current 에서 도는 systemd 서비스로 옮겨 간 뒤라, 스크립트는 아무도 읽지 않는 디렉터리에 썼고 워크플로는 돌고 있는 서비스와 포트를 두고 경합했을 것이다. 둘 다, 의존하던 pm2 launch 스크립트·process 파일과 함께 제거했다.
Similar pages by cosine similarity. Words after page name are term frequency.
AhaWiki
RecentChanges, TitleIndex, PageList, PageMap, WikiStatistics, Schema
Powered by AhaWiki